package com.project.mq.mqserver.core;

import com.project.mq.common.MqException;

/**
 * 使用这个类，来实现交换机的转发规则。
 * 同时也借助这个类验证 bindingKey 是否合法。
 */
public class Router {
    //bindingKey的构造规则如下：
    //bindingKey可以包含数字、字母和下划线。
    //使用点号（.）将整个bindingKey分成了若干部分。
    //允许存在星号（*）和井号（#）作为通配符，但是这些通配符只能作为独立的分段。
    public  boolean checkBindingKey(String bindingKey) {
        // 1. 允许是空字符串
        if (bindingKey.length() == 0) {
            //直接交换机/fanout的时候,bindingkey用不上,属于合法情况
            return true;
        }

        // 先判定基础构成
        for (int i = 0; i < bindingKey.length(); i++) {
            char ch = bindingKey.charAt(i);
            if (ch >= 'A' && ch <= 'Z') {
                continue;
            }
            if (ch >= 'a' && ch <= 'z') {
                continue;
            }
            if (ch >= '0' && ch <= '9') {
                continue;
            }
            if (ch == '_' || ch == '*' || ch == '#' || ch == '.') {
                continue;
            }
            return false;
        }

        // 再判定每个词的情况
        //检查 *或者#是否是独立的部分。
        //aaa.*.bbb 合法情况；aaa.a*.bbb 非法情况。
        //首先，"."在正则表达式中，是一个特殊的符号~~，

        // 此处是把.想当做原始文本来进行匹配。要想使用.原始文本，就需要转义。
        // 在正则中就需要使用 \.的方式来表示~~ 又因为，当前是要在Java的字符串中，来写入"\."这样的文本，
        // \又是一个特殊字符，就需要使用\\先对反斜杠转义，此时才能够真正录入\这个文本。
        // \这个转义字符在正则中才能生效。
        String[] words = bindingKey.split("\\.");
        for (String word : words) {
            // 检查 word 长度 > 1 并且包含了 * 或者 #，就是非法的格式了。
            if (word.length() > 1 && (word.contains("*") || word.contains("#"))) {
                return false;
            }
        }

        // 再判定相邻词的情况
        // 约定一下，通配符之间的相邻关系(人为约定的).
        // 1. aaa.#.#.bbb => 非法
        // 2. aaa.#.*.bbb => 非法
        // 3. aaa.*.#.bbb => 非法
        // 4. aaa.*.*.bbb => 合法
        //第四种比较好实现,前三种实现起来比较繁琐,但是功能性提升不大
        for (int i = 0; i < words.length - 1; i++) {
            // 连续两个#
            if (words[i].equals("#") && words[i + 1].equals("#")) {
                return false;
            }
            // # 连着 *
            if (words[i].equals("#") && words[i + 1].equals("*")) {
                return false;
            }
            // * 连着 #
            if (words[i].equals("*") && words[i + 1].equals("#")) {
                return false;
            }
        }
        return true;
    }


    public boolean checkRoutingKey(String routingKey) {
        if (routingKey.length() == 0) {
            //合法情况,fanout交换机的时候,routingkey用不上,可以设为"";
            return true;
        }

        // 数字字母下划线构成
        //使用.分割成若干部分
        for (int i = 0; i < routingKey.length(); i++) {
            char ch = routingKey.charAt(i);
            if (ch >= 'A' && ch <= 'Z') {
                continue;
            }
            if (ch >= 'a' && ch <= 'z') {
                continue;
            }
            if (ch >= '0' && ch <= '9') {
                continue;
            }
            if (ch == '_' || ch == '.') {
                continue;
            }
            return false;
        }
        return true;
    }

    //这个方法用来判定该消息是否可以转发给这个绑定对应的队列。
    public boolean route(ExchangeType exchangeType, Binding binding, Message message) throws MqException {
        // 根据不同的 exchangeType 进行不同的转发逻辑
        // DIRECT 的转发逻辑已经在外部判定过。
        if (exchangeType == ExchangeType.FANOUT) {
            //
            return routeFanout(binding, message);
        } else if (exchangeType == ExchangeType.TOPIC) {
            return routeTopic(binding, message);
        } else {
            throw new MqException("[VirtualHost Router] 未知的 exchangeType! exchangeType=" + exchangeType);
        }
    }

    private boolean routeFanout(Binding binding, Message message) {
        // 对于 fanout 类型，直接转发，不需要进行任何匹配。
        return true;
    }

    // bindingKey 表示任一个 token 可以匹配
    // 1. # 表示是一个通配符，可以匹配任意数量的 token。
    // 2. * 表示一个 token 都可以匹配。
    // 3. 其他内容都是普通字符串。
    // 4. # 不会连续出现，# 和 * 不能相邻。
    // routing key 不包含通配符。这个在发消息的时候校验
    //针对 bindingKey 的下标判定当前下标指向的部分，具体的情况如下：
    //
    //1.指向的是普通的字符串：此时要求和 routingKey 的对应下标指向的内容得完全一致!!!
    //2.指向的是 *：此时无论 routingKey 这边指向的是啥，都是双方同时下标前进~~
    //3.遇到了 #，并且如果 # 后面没有其他的内容了：直接返回 true，匹配成功!!!
    //4.遇到了 #，# 后面仍有其他内容：拿着 # 后面的部分~，去 routingKey 中查找找到后面的部分，在 routingKey 中出现的位置。
    //如果这个后面的部分，在routingKey不存在~~直接认为匹配失败!!!返回false!!!
    //如果后面的部分找到了就把routingKey的箭头指向这个位置之后,然后继续往后匹配~~
    //5.两个箭头移动过程中，如果同时到达双方的末尾也是匹配成功但是如界一个箭头先到了末尾，另一个还没到，则是匹配失败!!
    private boolean routeTopic(Binding binding, Message message) {
        String[] bindingTokens = binding.getBindingKey().split("\\.");
        String[] routingTokens = message.getRoutingKey().split("\\.");

        // 使用双指针的方式来实现匹配
        int bindingIndex = 0;
        int routingIndex = 0;

        //此处使用while更合适，每次循环，下标不一定就是+1，不适合使用for
        while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) {
            if (bindingTokens[bindingIndex].equals("*")) {
                // 情况2 如果是*，直接进入下一轮
                bindingIndex++;
                routingIndex++;
            } else if (bindingTokens[bindingIndex].equals("#")) {
                bindingIndex++;
                if (bindingIndex == bindingTokens.length) {
                    // 情况3 如果没有下一个位置，则直接返回 true
                    return true;
                }
                // 情况4 如果遇到 #，找到下一个位置的 token 在 routingKey 中的位置。
                //findNextMatch 这个方法用来查找该部分在 routingKey 的位置，返回该下标。没找到,返回-1
                routingIndex = findNextMatch(routingTokens, routingIndex, bindingTokens[bindingIndex]);
                if (routingIndex == -1) {
                    return false;
                }
                bindingIndex++;
                routingIndex++;
            } else {
                // 情况1 如果是普通字符，直接匹配内容是否相等，不相等则返回 false，相等直接进入下一轮
                if (!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) {
                    return false;
                }
                bindingIndex++;
                routingIndex++;
            }
        }

        //情况5 如果两方不同时结束，则视为匹配失败
        if (bindingIndex == bindingTokens.length && routingIndex == routingTokens.length) {
            return true;
        } else {
            return false;
        }
    }

    private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) {
        for (int i = routingIndex; i < routingTokens.length; i++) {
            if (routingTokens[i].equals(bindingToken)) {
                return i;
            }
        }
        return -1;
    }

}
